using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels;
using System.Net.Sockets;
using System.Security.Cryptography;

namespace CVS
{
    public class P2PClient : MarshalByRefObject, IRemoteClient
    {
        private const int MAXSIZE = 4;
        private const int K = 3;

        TcpChannel _channel;
        IRemoteClient _obj = null;
        private static IGestorRepositorios _local_manager = new GestorRepositorios();
        private static IGestorRepositorios _remote_manager = new GestorRepositorios();
        private static string _ipAddress;
        private static string _serverAddress;
        private static string _predecessor, _sucessor;
        private static int _rep_id;
        private static List<string> _finger_list = new List<string>();


        #region Constructors&Initializers

        public P2PClient(){}

        public void Initialize(TcpChannel channel, string address)
        {
            _rep_id = 1;

            _channel = channel;
            IpAddress = address;

            //Register client as a remote object
            RemotingConfiguration.RegisterWellKnownServiceType(
                                typeof(P2PClient),
                                "MyRemoteClient",
                                WellKnownObjectMode.Singleton);
        }

        #endregion

        #region Getters&Setters

        public string IpAddress
        {
            get { return _ipAddress; }
            set { _ipAddress = value; }
        }

        public string GetSucessor()
        {
            return _sucessor;
        }

        public void SetSucessor(string address)
        {
            _sucessor = address;
        }

        public string GetPredecessor()
        {
            return _predecessor;
        }

        public void SetPredecessor(string address)
        {
            _predecessor = address;
        }

        public int GetRepId()
        {
            return _rep_id;
        }

        public int GetNextRepId(string original_address, string address)
        {
            IRemoteClient obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + address + "/MyRemoteClient");
            if (original_address.CompareTo(obj.GetSucessor()) == 0)
            {
                return obj.GetRepId();
            }
            else
            {
                int this_id = obj.GetRepId();
                int next_id = obj.GetNextRepId(original_address, obj.GetSucessor());

                return Math.Max(this_id, next_id);
            }
        }
        #endregion

        #region RingRelatedMethods

        public string Connect(string address)
        {
            _serverAddress = address;

            return UpdateConnectionRing(address, address);
        }

        public void Disconnect()
        {
            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");

            string sucessor = _obj.GetSucessor();
            string predecessor = _obj.GetPredecessor();

            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + sucessor + "/MyRemoteClient");

            //Send this client's repositories to its sucessor
            foreach (IRepositorio rep in _remote_manager.Repositorios)
            {
                _obj.TransferRepositories(rep);
            }

            _obj.SetPredecessor(predecessor);

            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + predecessor + "/MyRemoteClient");
            _obj.SetSucessor(sucessor);

            _remote_manager.Repositorios.Clear();
        }

        public void GetSucessorData()
        {
            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");

            IRemoteClient sucessor = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + _obj.GetSucessor() + "/MyRemoteClient");
            _remote_manager = sucessor.TransferData(IpAddress);
        }

        public IGestorRepositorios TransferData(string address)
        {
            IGestorRepositorios manager_data = new GestorRepositorios();

            //Get the repositories that should be in the given address
            foreach (IRepositorio rep in _remote_manager.Repositorios)
            {
                if (Convert(rep.Id) <= Convert(address))
                {
                    manager_data.Repositorios.Add(rep);
                }
            }

            //Remove the repositories that will be transfered to the given address
            foreach (IRepositorio rep in manager_data.Repositorios)
            {
                IRepositorio temp = _remote_manager.ObtemRepositorio(rep.Id);
                _remote_manager.Repositorios.Remove(temp);
            }

            return manager_data;
        }

        public void TransferRepositories(IRepositorio rep)
        {
            _remote_manager.Repositorios.Add(rep);
        }

        public string UpdateConnectionRing(string connection_address, string original_address) {
            //This is the first client - sucessor and predecessor are the same
            if (connection_address.CompareTo("") == 0)
            {
                _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");

                _obj.SetSucessor(IpAddress);
                _obj.SetPredecessor(IpAddress);

                return IpAddress;
            }
            else
            {
                try
                {
                    _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + connection_address + "/MyRemoteClient");
                    BigInteger server_key = Convert(connection_address);
                    BigInteger own_key = Convert(IpAddress);

                    string server_sucessor = _obj.GetSucessor();
                    string server_predecessor = _obj.GetPredecessor();

                    //In this case, there is more than 1 server already connected
                    if (connection_address.CompareTo(server_sucessor) != 0)
                    {
                        BigInteger sucessor_key = Convert(server_sucessor);
                        BigInteger predecessor_key = Convert(server_predecessor);

                        //New client is going to occupy the space between the client it has connected to and its sucessor
                        if ((own_key < sucessor_key) && (own_key > server_key))
                        {

                            //Update sucessor of client predecessor
                            _obj.SetSucessor(IpAddress);

                            //Set new client pointers
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");
                            _obj.SetSucessor(server_sucessor);
                            _obj.SetPredecessor(connection_address);

                            //Update predecessor of client sucessor
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + _obj.GetSucessor() + "/MyRemoteClient");
                            _obj.SetPredecessor(IpAddress);

                        }
                        //New client is going to occupy the space between the client it has connected to and its predecessor
                        else if ((own_key < server_key) && (own_key > predecessor_key))
                        {
                            //Update predecessor of client sucessor
                            _obj.SetPredecessor(IpAddress);

                            //Set new client pointers
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");
                            _obj.SetSucessor(connection_address);
                            _obj.SetPredecessor(server_predecessor);

                            //Update sucessor of client predecessor
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + _obj.GetPredecessor() + "/MyRemoteClient");
                            _obj.SetSucessor(IpAddress);
                        }
                        //No place in between the existing servers. Place the new one at the end of the ring
                        else if (server_sucessor.CompareTo(original_address) == 0)
                        {
                            //Update sucessor of client predecessor
                            _obj.SetSucessor(IpAddress);

                            //Set new client pointers
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");
                            _obj.SetPredecessor(connection_address);
                            _obj.SetSucessor(server_sucessor);

                            //Update sucessor of client predecessor
                            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + _obj.GetSucessor() + "/MyRemoteClient");
                            _obj.SetPredecessor(IpAddress);
                        }
                        else
                        {
                            UpdateConnectionRing(server_sucessor, original_address);
                        }
                    }
                    //In this case, there is only one server in the ring
                    else
                    {
                        //Update sucessor of client sucessor
                        _obj.SetSucessor(IpAddress);
                        //Update predecessor of client predecessor
                        _obj.SetPredecessor(IpAddress);

                        //Set new client pointers
                        _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");
                        _obj.SetSucessor(connection_address);
                        _obj.SetPredecessor(connection_address);
                    }

                    return connection_address;
                }
                catch (System.Net.Sockets.SocketException)
                {
                    return null;
                }
                catch (System.IO.IOException)
                {
                    return null;
                }
                catch (System.Runtime.Remoting.RemotingException)
                {
                    return null;
                }
            }
        }

        public string FindSucessor(BigInteger key, string address)
        {
            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + address + "/MyRemoteClient");
            
            string sucessor = _obj.GetSucessor();

            if (key >= Convert(sucessor))
            {
                return FindSucessor(key, sucessor);
            }
            else
            {
                return sucessor;
            }

        }

        public void CreateFingerTable()
        {
            for (int i = 1; i < MAXSIZE*8; i++)
            {
                BigInteger key = (Convert(IpAddress) + 2 ^ (i-1)) % (2^(MAXSIZE*8));
                _finger_list.Add(FindSucessor(key, IpAddress));
            }
        }

        public List<string> RetrieveFingerTable()
        {
            return _finger_list;
        }

        public string RetrieveRingInformation()
        {
            IRemoteClient _object = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");

            return IpAddress + "#" + _object.GetSucessor() + "#" + _object.GetPredecessor() + "#" + Convert(IpAddress);
        }

        public string FindServer(string id, string address)
        {
            IRemoteClient _object = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + address + "/MyRemoteClient");

            string sucessor = _object.GetSucessor();
            string predecessor = _object.GetPredecessor();

            //There is only 1 server. It must hold everything
            if (address.CompareTo(sucessor) == 0)
            {
                return address;
            }

            BigInteger rep_key = Convert(id);

            //This is the beginning of the ring
            if (Convert(predecessor) > Convert(address))
            {
                //Repository key is greater than predecessor's key -> it must be located at the beggining of the ring
                if (rep_key >= Convert(predecessor))
                {
                    return address;
                }
                //Repository key is lesser than current address's key -> it must be located here
                else if (rep_key <= Convert(address))
                {
                    return address;
                }
                //Repository key is greater than current address's key -> forward search to sucessor
                else
                {
                    return FindServer(id, sucessor);
                }
            }
            else if ((rep_key <= Convert(address)) && (rep_key > Convert(predecessor)))
            {
                return address;
            }
            else
            {
                return FindServer(id, sucessor);
            }
        }

        #endregion

        #region ServerMethods

        public IRepositorio RemoteCheckout(string id)
        {
            return _remote_manager.ObtemRepositorio(id);
        }

        public int RemoteCheckin(IRepositorio repository)
        {
            int returnValue = 0;
            IRepositorio rep = _remote_manager.ObtemRepositorio(repository.Id);

            int versionRepInServer = rep.Versao;
            int versionRepInClient = repository.Versao;

            if (versionRepInClient >= versionRepInServer)
            {
                versionRepInServer++;
                repository.Versao = versionRepInServer;

                //Remove last version from repository list
                _remote_manager.Repositorios.Remove(rep);

                //Add the new version to the repository list
                _remote_manager.Repositorios.Add(repository);

                foreach (string address in repository.Replicas)
                {
                    _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + address + "/MyRemoteClient");
                    _obj.RemoteCheckinProp(repository);
                }

                //-1 significa que o check-in foi realizado com sucesso
                returnValue = -1;
                System.Console.WriteLine("Repository checked-in.");
            }
            else
            {
                //caso a versao do servidor seja mais recente n actualiza nada e envia para o cliente o numero da versao existente
                returnValue = versionRepInServer;
                System.Console.WriteLine("Repository checkin rejected - newer version already exists.");
            }

            return returnValue;
        }

        public void RemoteCheckinProp(IRepositorio repository)
        {
            //Remove last version from repository list
            IRepositorio rep = _remote_manager.ObtemRepositorio(repository.Id);
            _remote_manager.Repositorios.Remove(rep);

            //Add the new version to the repository list
            _remote_manager.Repositorios.Add(repository);
        }

        public Boolean RemoteDeleteRep(string rep_id)
        {
            IRepositorio repository = _remote_manager.ObtemRepositorio(rep_id);
            //Delete repository
            _remote_manager.Repositorios.Remove(repository);

            foreach (string address in repository.Replicas)
            {
                _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + address + "/MyRemoteClient");
                _obj.RemoteDeleteRepProp(rep_id);
            }

            return true;
        }

        public void RemoteDeleteRepProp(string rep_id)
        {
            IRepositorio rep = _remote_manager.ObtemRepositorio(rep_id);
            _remote_manager.Repositorios.Remove(rep);
        }

        public IRepositorio RemoteCreateRep(string name, int rep_id)
        {
            IRepositorio rep = new RepositorioBase();
            rep.Id = rep_id.ToString();
            rep.Nome = name;
            rep.Ficheiros = new List<IFicheiro>();
            rep.EnderecoReplicaPrincipal = IpAddress;

            string sucessor = GetSucessor();
            for (int i = 0; i < K - 1; i++)
            {
                if (sucessor.CompareTo(IpAddress) != 0)
                {
                    _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + sucessor + "/MyRemoteClient");
                    _obj.RemoteCreateRepProp(name, rep_id, IpAddress);

                    rep.Replicas.Add(sucessor);
                }
                else
                {
                    break;
                }

                sucessor = _obj.GetSucessor();

            }

            _remote_manager.Repositorios.Add(rep);

            _rep_id = rep_id;
            _rep_id++;

            return rep;
        }

        public IRepositorio RemoteCreateRepProp(string name, int rep_id, string main_address)
        {
            _rep_id = rep_id;
            IRepositorio rep = new RepositorioBase();
            rep.Id = rep_id.ToString();
            rep.Nome = name;
            rep.Ficheiros = new List<IFicheiro>();
            rep.EnderecoReplicaPrincipal = main_address;

            _remote_manager.Repositorios.Add(rep);

            return rep;
        }

        public string RemoteListFiles(string rep_id)
        {
            IRepositorio rep = _remote_manager.ObtemRepositorio(rep_id);
            if (rep != null)
            {
                string filelist = "";
                foreach (IFicheiro file in rep.Ficheiros)
                {
                    if (filelist.CompareTo("") == 0)
                    {
                        filelist = file.Nome + "@" + file.Conteudo.Length;
                    }
                    else
                    {
                        filelist += "#" + file.Nome + "@" + file.Conteudo.Length;
                    }
                }
                return filelist;
            }
            else
            {
                return null;
            }
        }

        #endregion

        #region ClientMethods

        public IRepositorio CheckoutRep(string id)
        {
            string server_address = FindServer(id, IpAddress);
            IRemoteClient obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + server_address + "/MyRemoteClient");
            IRepositorio rep = obj.RemoteCheckout(id);
            
            //If no repository with the given id was found on the server, return null
            if (rep == null)
            {
                return null;
            }

            IRepositorio temp;
            //If the repository already exists, remove it from the list
            if ((temp = _local_manager.ObtemRepositorio(rep.Id)) != null)
            {
                _local_manager.Repositorios.Remove(temp);
            }
            rep.Alterado = false;
            //Add the repository to the list
            _local_manager.Repositorios.Add(rep);

            return rep;
        }

        public Boolean CheckInRepository(string rep_id)
        {
            IRepositorio rep = _local_manager.ObtemRepositorio(rep_id);

            string server_address = FindServer(rep_id, IpAddress);
            IRemoteClient obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + server_address + "/MyRemoteClient");

            int returnValue = obj.RemoteCheckin(rep);
            rep.Versao++;
            if (returnValue == -1)
            {
                //Update repository state
                rep.Alterado = false;

                foreach (IFicheiro file in rep.Ficheiros)
                {
                    file.Alterado = false;
                }
                return true;
            }
            else
            {
                return false;
            }
        }

        public Boolean DeleteRepository(string rep_id)
        {
            IRepositorio rep = _local_manager.ObtemRepositorio(rep_id);
            string server_address = FindServer(rep_id, IpAddress);
            IRemoteClient obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + server_address + "/MyRemoteClient");

            if (obj.RemoteDeleteRep(rep_id))
            {
                _local_manager.Repositorios.Remove(rep);
            }

            return true;
        }


        public IRepositorio CreateRep(string name)
        {
            int rep_id;

            //Determines the next available id (in all servers) for the new repository
            IRemoteClient obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + IpAddress + "/MyRemoteClient");
            rep_id = GetNextRepId(IpAddress, IpAddress);

            obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + FindServer(rep_id.ToString(), IpAddress) + "/MyRemoteClient");

            //Create repository in server
            IRepositorio rep =  obj.RemoteCreateRep(name, rep_id);

            rep.Alterado = true;
            //Add a local copy of the repository to the client list
            _local_manager.Repositorios.Add(rep);

            return rep;
        }
 

        public string ListFiles(string rep_id)
        {
            _obj = (IRemoteClient)Activator.GetObject(typeof(IRemoteClient), "tcp://" + FindServer(rep_id.ToString(), IpAddress) + "/MyRemoteClient");
            return _obj.RemoteListFiles(rep_id);
        }

        public List<IFicheiro> ShowRepContent(string id)
        {
            return _local_manager.ObtemRepositorio(id).Ficheiros;
        }

        public void CreateFile(string name, string rep_number)
        {
            IRepositorio rep = _local_manager.ObtemRepositorio(rep_number);

            //Create the new file
            IFicheiro file = new FicheiroBase();
            file.Nome = name;
            file.Conteudo = "Ficheiro vazio";
            file.Alterado = true;

            //Add the new file to the current repository file list
            rep.Ficheiros.Add(file);
        }

        public string ShowFileContent(string rep_number, string filename)
        {
            IRepositorio rep = _local_manager.ObtemRepositorio(rep_number);
            return rep.ObtemFicheiro(filename).Conteudo;
        }

        public bool SaveFile(string rep_number, string filename, string content)
        {
            IRepositorio rep = _local_manager.ObtemRepositorio(rep_number);
            IFicheiro file = rep.ObtemFicheiro(filename);

            //If file content has changed, save the new content and return true
            if (file.Conteudo.CompareTo(content) != 0)
            {
                file.Conteudo = content;
                file.Alterado = true;
                return true;
            }
            //Otherwise do nothing and return false
            else
            {
                return false;
            }
        }

        public void DeleteFile(string rep_id, string file_name, int previousOperation)
        {
            if (previousOperation == 1)
            {
                IRepositorio rep = _local_manager.ObtemRepositorio(rep_id);
                //previousOperation = check-out
                foreach (IFicheiro file in rep.Ficheiros)
                {
                    if (file.Nome.CompareTo(file_name) == 0)
                    {
                        rep.Ficheiros.Remove(file);
                        break;
                    }
                }
            }
            else
            {
                //previousOperation = lista_ficheiros
                int retValue = 0; // _obj.DeleteFile(rep_id, file_name);
                switch (retValue)
                {
                    case -1:
                        MessageBox.Show("Unable to delete file -> Unknown Repository");
                        break;
                    case 0:
                        MessageBox.Show("Unable to delete file -> Unknown File");
                        break;
                    case 1:
                        MessageBox.Show("File Deleted");
                        break;
                    default:
                        MessageBox.Show("Unable to delete file -> Unknown Error");
                        break;
                }
            }
        }

        #endregion

        #region ServerCommands

        public string Print()
        {
            string result ="";
            foreach (IRepositorio repositorio in _remote_manager.Repositorios)
            {
                if (repositorio.EnderecoReplicaPrincipal.CompareTo(IpAddress) == 0)
                {
                    result += "Id: " + repositorio.Id + ", Name: " + repositorio.Nome + ", Type: Primary#";
                }
                else
                {
                    result += "Id: " + repositorio.Id + ", Name: " + repositorio.Nome + ", Type: Secundary#";
                }
            }
            return result;
        }

        public void Load(string filename)
        {
            try
            {
                _remote_manager.CarregaRepositorios(filename);
            }
            catch (System.IO.FileNotFoundException)
            {
                MessageBox.Show("File not found.");
            }
        }

        #endregion

        #region UtilitaryMethods

        public BigInteger Convert(string address)
        {
            byte[] data = Encoding.UTF8.GetBytes(address);
            byte[] result;
            SHA1 sha = new SHA1CryptoServiceProvider();
            // This is one implementation of the abstract class SHA1. 
            result = sha.ComputeHash(data);

            byte[] new_result = new byte[MAXSIZE];
            for (int i = 0; i < MAXSIZE; i++)
                new_result[i] = result[i];

            return new BigInteger(new_result);
        }

        #endregion

    }
}
